#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on Sun Mar  4 18:18:56 2012

@author: Sergueï Kilimoff-Goriatchkine
@mail: serge.kilimoff@gmail.com

@version : 11-03-2012
@licence : BSD -- http://www.freebsd.org/copyright/license.html
"""
#
#
# Import from .TMX file construct with TILED.
# Export file with cPickle.
# For import file, use cPicke.load(file)
# 
# For decoup images into smaller tiles,
# this script uses Python Image Library (PIL).
# For read .TMX, this script uses xml.etree,
# into the Python Standard Library
#
#
#==============================================================================
# Structure for construct file
#==============================================================================
# {level}
#     'tiles':[tiles_index]
#         n*{tiles}
#              'image':cStringIO
#              n*'<attributes>':str|int|...<values>
#     'layers':[layers_index]
#         n*{layer}
#             n*'<attributes>':str|int|...<values>
#             n*(x, y): {tiles}
# n*'<attributes>':str|int|...<values>
#==============================================================================


#==============================================================================
# T0D0 LIST ( french )
#==============================================================================
# TODO: réécrire les docstrings en anglais
# TODO: récuperer et traiter les layers d'objets
#==============================================================================


#==============================================================================
# DEPENDANCE
#==============================================================================
from xml.etree.ElementTree import ElementTree
from PIL import Image
from StringIO import StringIO
import argparse
import cPickle
import os.path


#==============================================================================
# MAIN CLASS
#==============================================================================
class Converter(object):
    """ Permet de convertir les fichiers .tmx, produit à partir
        de Tiled Map Editor."""
    xml_path = str()
    def __new__(cls):
        raise AttributeError("This class cannot be instancied")


    #===========================================================================
    # INTERNAL FUNCTIONS
    #===========================================================================
    @classmethod
    def _normalize_type_attribute(cls, attr):
        """ Retourne l'attribut transformé avec le bon type """
        try:
            return int(attr)
        except ValueError:
            if attr == "True":
                return True
            elif attr == "False":
                return False
            else:
                return attr
    
    @classmethod
    def _get_attributes_data(cls, datas):
        """ Retourne les attributs d'un objet depuis le fichier tmx """
        attributes = dict()
        for name, attr in datas.items():
            attribute = Converter._normalize_type_attribute(attr)
            attributes.update({name:attribute})
        return attributes
    
    
    #===========================================================================
    # LEVEL FUNCTIONS
    #===========================================================================
    @classmethod
    def _extract_level_data(cls, datas):
        """ Extrait les données du niveau depuis le fichier tmx """
        return cls._get_attributes_data(datas)
    
    
    #===========================================================================
    # LAYERS FUNCTIONS
    #===========================================================================
    @classmethod
    def _extract_layers_data(cls, datas, size, offset):
        """ Extrait une liste de calque depuis le fichier tmx """
        layers = list()
        for data in datas:
            layer = dict()
            layer.update(cls._get_attributes_data(data))
            layer.update(cls._extract_layer_data(data.findall('data/tile'), \
                        size, offset))
            layers.append(layer)
        return layers
    
    @classmethod
    def _extract_layer_data(cls, datas, size, offset):
        """ Extrait les données du calque depuis le fichier xml """
        layer = dict()
        for index, data in enumerate(datas):
            gid = cls._normalize_type_attribute(data.get('gid'))
            if gid:
                posx, posy = _coordinates_since_index(index, size, offset)
                layer.update({(posx, posy): cls._get_tile_gid(gid)})
        return layer
    
    @classmethod
    def _get_tile_gid(cls, gid):
        """ Retourne la gid correct de la tuile """
        return gid - 1


    #===========================================================================
    # TILES FUNCTIONS
    #===========================================================================
    @classmethod
    def _extract_tileset_data(cls, datas):
        """ Extrait les données d'un tileset depuis le fichier xml """
        tileset = list()
        for data in datas:
            # Normalize path for image
            image = data.find('image')
            image_path = cls._normalize_type_attribute(image.get('source'))
            image_path = os.path.join(cls.xml_path, image_path)
            image_path = os.path.normpath(image_path)
            
            
            size = cls._normalize_type_attribute(image.get('width')), \
                   cls._normalize_type_attribute(image.get('height'))
            offset = cls._normalize_type_attribute(data.get('tilewidth')), \
                   cls._normalize_type_attribute(data.get('tileheight'))
            
            tiles = cls._extract_tiles_data(data.findall('tile'), \
                    image_path, size, offset)
            tileset.extend(tiles)
        return tileset
    
    @classmethod
    def _extract_tiles_data(cls, datas, image_path, size, offset):
        """ Extrait les données des tuiles depuis le fichier xml """
        tiles = list()
        count_tiles = (size[0] / offset[0]) * (size[1] / offset[1])
        
        image = Image.open(image_path)
        
        tiles_attributes = cls._extract_tiles_attributes(datas)
        
        for index in xrange(count_tiles):
            tile = dict()
            tile.update(cls._get_string_image(image, index, size, offset))
            tile.update(cls._get_tile_attribute(index, tiles_attributes))
            tiles.append(tile)
        
        return tiles
   
    @classmethod
    def _extract_tiles_attributes(cls, datas):
        """ Retourne les attributs des tuiles depuis le fichier tmx """
        tiles_attributes = dict()
        for data in datas:
            gid = cls._normalize_type_attribute(data.get('id'))
            tile_attributes = dict()
            for attribute in data.findall('properties/property'):
                tile_attributes.update({
                    attribute.get('name'): cls._normalize_type_attribute(\
                                            attribute.get('value'))
                    })
            tiles_attributes.update({gid:tile_attributes})
        return tiles_attributes
    
    @classmethod
    def _get_tile_attribute(cls, index, tiles_attributes):
        """ Retourne les attributs d'une tuile précise depuis la structure
            d'attributs de toutes les tuiles """
        tile_attributes = tiles_attributes.get(index, {})
        return tile_attributes


    #===========================================================================
    # IMAGE MANIPULATION
    #===========================================================================
    @classmethod
    def _get_string_image(cls, image, index, size, offset):
        """ Retourne un morceau d'image sous forme d'une chaine de caractère
            depuis le fichier image source """
        left, top = _coordinates_since_index(index, size, offset)
        right, bottom = left + offset[0], top + offset[1]
        sub_image = image.crop((left, top, right, bottom))
        buff = StringIO()
        sub_image.save(buff, 'PNG')
        buff.seek(0)
        string_image = {'image':buff.read()}
        buff.close()
        return string_image


    #===========================================================================
    # POST MANIPULATION FUNCTION
    #===========================================================================
    @classmethod
    def _do_links_tiles_to_layers(cls, tiles, layers):
        """ Fait le lien entre les tuiles et les calques, en remplaçant le gid
            des tuiles par les tuiles elle-mêmes """
        for layer in layers:
            coordinates = [pos for pos in layer if isinstance(pos, tuple)]
            for coordinate in coordinates:
                gid = layer[coordinate]
                layer[coordinate] = tiles[gid]
    
    
    #===========================================================================
    # CONVERT FUNCTIONS AND API
    #===========================================================================
    @classmethod
    def convert_xml_to_python(cls, xml):
        """ Converti du fichier tmx vers un objet python """
        xmltree = ElementTree()
        xmltree.parse(xml)
        
        level = dict()
        level.update(cls._get_attributes_data(xmltree.getroot()))
        
        offset = (level['tilewidth'], level['tileheight'])
        size = (level['width'] * offset[0], level['height'] * offset[1])

        
        level['layers'] = cls._extract_layers_data(xmltree.iter('layer'), \
                            size, offset)
        level['tiles'] = cls._extract_tileset_data(xmltree.iter('tileset'))
        return level
    
    @classmethod
    def convert_python_to_file(cls, path, instance):
        """ Enregistre l'objet python dans un fichier """
        with open(path, 'wb') as _file:
            cPickle.dump(instance, _file, -1)
        return True
    
    @classmethod
    def convert_xml_to_file(cls, xml, path):
        """ Converti un fichier tmx vers un fichier. Conversion vers
            une structure python """
        instance = cls.convert_xml_to_python(xml)
        cls._do_links_tiles_to_layers(instance['tiles'], instance['layers'])
        response = cls.convert_python_to_file(path, instance)
        return response
        

#==============================================================================
# DERIVATED CLASS
#==============================================================================
class OptimizeConverter(Converter):
    """ Save only tiles used by layers. Others tiles is not saved. """
    used_tile = list()
    
    @classmethod
    def _get_tile_gid(cls, gid):
        """ Return gid of the tile, and append this gid into a list """
        gid = gid - 1
        if not gid in cls.used_tile:
            cls.used_tile.append(gid)
        return gid
    
    @classmethod
    def _extract_tiles_data(cls, datas, image_path, size, offset):
        """ If this tile is used in layers, transform this tile into python
        format """
        tiles = list()
        count_tiles = (size[0] / offset[0]) * (size[1] / offset[1])
        image = Image.open(image_path)
        
        tiles_attributes = cls._extract_tiles_attributes(datas)
        
        for index in [nb for nb in xrange(count_tiles) if nb in cls.used_tile]:
            tile = dict()
            tile.update(cls._get_string_image(image, index, size, offset))
            tile.update(cls._get_tile_attribute(index, tiles_attributes))
            tiles.append(tile)
        
        return tiles
    
    @classmethod
    def _do_links_tiles_to_layers(cls, tiles, layers):
        """ Since tiles used into layers, make links between layers
            and tiles """
        for layer in layers:
            coordinates = [pos for pos in layer if isinstance(pos, tuple)]
            for coordinate in coordinates:
                gid = layer[coordinate]
                index = cls.used_tile.index(gid)
                layer[coordinate] = tiles[index]


#==============================================================================
# PRIVATE METHOD
#==============================================================================
def _coordinates_since_index(index, size, offset):
    """ Return coordinate tuple(x,y) """
    assert isinstance(index, int) 
    assert isinstance(offset, tuple) and isinstance(size, tuple)
    width = size[0] / offset[0]
    posx = (index % width) * offset[0]
    posy = (index / width) * offset[1]
    return posx, posy


#==============================================================================
# OPTIONS FOR COMMAND LINE
#==============================================================================
def command():
    """ Commands for lauch this script """
    description = """
        Convert TMX file to other file.
        TMX file is create from Tiled Map Editor.
        Other file is a Python Dict object saved with cPickle.
        
        Optimize flag save only tiles really used.
        """
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('tmx', action='store', help="TMX file to convert")
    parser.add_argument('new_file', action='store', help="Destination file")
    parser.add_argument('-o', '--optimize', action='store_true', \
                        default=False, help="Pass to flag OPTIMIZE")

    args = parser.parse_args()
    path_xml = os.path.abspath(args.tmx)
    path_new = os.path.abspath(args.new_file)
    
    path_xml_ext = os.path.splitext(path_xml)[1]
    if path_xml_ext != ".tmx":
        print("The file extension {0} is not accepted.\
            \nYou're need used .tmx".format(path_xml_ext))
        return False
    
    if args.optimize:
        converter = OptimizeConverter
    else:
        converter = Converter
    converter.xml_path = os.path.split(path_xml)[0]
    converter.convert_xml_to_file(path_xml, path_new)


#==============================================================================
# LAUNCH SCRIPT
#==============================================================================
if __name__ == "__main__":
    command()